﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Text.RegularExpressions;

namespace StatelyCompiler
{
    public class Lexer
    {        
        public enum State { 
            Unknown, 
            Starting, 
            
            PendingWildcardOrState, 
            PendingState,
            PendingOperator,
            PendingArrayState,
            PendingCompletionState,                        
            PendingStatementCompletion,

            BuildingStartState,
            BuildingState,
            BuildingArrayState,
            BuildingCompletionState, 
            
            BuildingTransitionTo,             
            BuildingDerailTo, 
            
            Ignore }

        State _state = State.Starting;

        int _colNum;
        int _rowNum;
        bool _newLine = false;

        StringBuilder _builder;

        Regex _stateNameRegex = new Regex(@"^[a-zA-Z0-9_\-]*$");
        Regex _initalStateNameCharacterRegex = new Regex("[a-zA-Z]$");

        State _precommentstate = State.Unknown;

        public List<Token> Lex(FileStream stream)
        {
            List<Token> tokens = new List<Token>();

            _colNum = 0;
            _rowNum = 0;


            if (!stream.CanRead) throw new InvalidOperationException("Stream must be opened for reading");

            using (StreamReader reader = new StreamReader(stream))
            {
                while (reader.Peek() != -1)
                {
                    try
                    {
                        char val = (char)reader.Read();

                        if (val == '#') { Hash(tokens); continue; }

                        if (_state == State.Ignore) continue;

                        if (val == '\r') { CarriageReturn(tokens); continue; }
                        if (val == '\n') { LineFeed(tokens); continue; }
                        if (val == ' ' || val == '\t') { Space(tokens); continue; }
                        if (val == '[') { OpenBrace(tokens); continue; }
                        if (val == ']') { CloseBrace(tokens); continue; }
                        if (val == '=') { Equal(tokens); continue; }
                        if (val == '>') { GreaterThan(tokens); continue; }
                        if (val == '/') { Slash(tokens); continue; }
                        if (val == '*') { Asterix(tokens); continue; }
                        if (val == ';') { SemiColon(tokens); continue; }
                        if (val == '|') { Pipe(tokens); continue; }
                        if (val == '{') { OpenCurly(tokens); continue; }
                        if (val == '}') { CloseCurly(tokens); continue; }
                        if (val == ',') { Comma(tokens); continue; }
                        if (_stateNameRegex.IsMatch(val.ToString())) { Character(val, tokens); continue; }

                        Exception("Unexpected character '{0}'.".F(val));
                    }
                    finally
                    {
                        if (_newLine)
                        {
                            _colNum = 0;
                            _rowNum++;
                        }
                        else
                        {
                            _colNum++;
                        }

                        _newLine = false;
                    }
                }
            }

            return tokens;
        }

        private void Comma(List<Token> tokens)
        {
            if (_state != State.BuildingArrayState) Exception("Unexpected ',' located.");

            AssertStateIsNotNull();

            AddCurrentStateAsArrayStateToTokens(tokens);

            _state = State.PendingArrayState;
        }

        private void Hash(List<Token> tokens)
        {
            if (_state != State.Ignore)
            {
                _precommentstate = _state;
                _state = State.Ignore;
            }
            else
            {
                _state = _precommentstate;
            }
        }

        private void SemiColon(List<Token> tokens)
        {
            if (_state != State.PendingOperator && _state != State.BuildingState && _state != State.PendingStatementCompletion) Exception("Unexpected character ';'.");

            if (_state == State.BuildingState) AddCurrentStateToTokens(tokens);

            tokens.Add(TokenFor(Token.TokenType.Break));

            _state = State.PendingWildcardOrState;
        }

        private void CarriageReturn(List<Token> tokens)
        {
            HandleNewLineOperations(tokens);
        }

        private void LineFeed(List<Token> tokens)
        {
            HandleNewLineOperations(tokens);

            _newLine = true;
        }

        private void HandleNewLineOperations(List<Token> tokens)
        {
            if (_state == State.BuildingState)
            {
                AddCurrentStateToTokens(tokens);
                _state = State.PendingOperator;
            }

            if (!(_state.In(State.PendingOperator, State.PendingState, State.PendingWildcardOrState, State.Starting))) Exception("Unexpected New Line.");
        }

        private void Asterix(List<Token> tokens)
        {
            if (_state != State.PendingWildcardOrState) Exception("Unexpected character '*'.");

            tokens.Add(TokenFor(Token.TokenType.Wildcard));

            _state = State.PendingOperator;
        }

        private void Character(char val, List<Token> tokens)
        {
            if (
                _state != State.PendingWildcardOrState &&
                _state != State.PendingState &&
                _state != State.PendingArrayState &&
                _state != State.PendingCompletionState &&                
                _state != State.BuildingStartState &&
                _state != State.BuildingState &&
                _state != State.BuildingArrayState &&
                _state != State.BuildingCompletionState                
                ) Exception("Unexpected character '{0}'.".F(val));

            if (_state == State.PendingState ||
                _state == State.PendingWildcardOrState) 
            { 
                _state = State.BuildingState; 
                _builder = new StringBuilder(); 
            }
            else if (_state == State.PendingCompletionState)
            {
                _state = State.BuildingCompletionState;
                _builder = new StringBuilder(); 
            }
            else if (_state == State.PendingArrayState)
            {
                _state = State.BuildingArrayState;
                _builder = new StringBuilder(); 
            }

            _builder.Append(val);

            if (_builder.Length == 1 &&
                !_initalStateNameCharacterRegex.IsMatch(_builder[0].ToString()))
            {
                Exception("Unexpected character '{0}'.".F(_builder[0]));
            }
        }

        private void Slash(List<Token> tokens)
        {
            HandleStartOperatorState(tokens, '/');

            _state = State.BuildingDerailTo;
        }

        private void GreaterThan(List<Token> tokens)
        {
            if (_state != State.BuildingTransitionTo && _state != State.BuildingDerailTo) Exception("Unexpected '>' located.");

            tokens.Add(TokenFor(_state == State.BuildingTransitionTo ? Token.TokenType.TransitionTo : Token.TokenType.Derail));

            _state = State.PendingState;
        }

        private void OpenCurly(List<Token> tokens)
        {
            if (_state != State.PendingState) Exception("Unexpected '{' located.");

            _state = State.PendingArrayState;
        }

        private void Equal(List<Token> tokens)
        {
            HandleStartOperatorState(tokens, '=');

            _state = State.BuildingTransitionTo;
        }

        private void OpenBrace(List<Token> tokens)
        {
            if (_state != State.Starting &&
                _state != State.PendingState &&
                _state != State.PendingWildcardOrState) Exception("Unexpected '[' located.");

            _builder = new StringBuilder();

            if (_state == State.Starting)
            {
                _state = State.BuildingStartState;
            }
            else
            {
                _state = State.PendingCompletionState;
            }
        }

        private void Pipe(List<Token> tokens)
        {
            if (_state != State.PendingState) Exception("Invalid '|' located.");

            tokens.Add(TokenFor(Token.TokenType.Pipe));

            _state = State.PendingOperator;
        }

        private void CloseBrace(List<Token> tokens)
        {
            if (_state != State.BuildingStartState &&
                _state != State.BuildingCompletionState
                ) Exception("Unexpected ']' located.");

            AssertStateIsNotNull();

            if (_state == State.BuildingCompletionState)
            {
                AddCurrentStateAsCompletionToken(tokens);
            }
            else
            {
                AddCurrentStateAsStartStateToTokens(tokens);
            }
        }

        private void CloseCurly(List<Token> tokens)
        {
            if (_state != State.BuildingArrayState) Exception("Unexpected '}' located.");

            AssertStateIsNotNull();

            AddCurrentStateAsArrayStateToTokens(tokens);

            _state = State.PendingStatementCompletion;
        }

        private void Space(List<Token> tokens)
        {
            if (_state == State.BuildingState) { AddCurrentStateToTokens(tokens); _state = State.PendingOperator; }
        }

        private void AddCurrentStateToTokens(List<Token> tokens)
        {
            AssertStateIsNotNull();

            tokens.Add(TokenFor(Token.TokenType.State, _builder.ToString()));

            _state = State.PendingOperator;
        }

        private void AddCurrentStateAsStartStateToTokens(List<Token> tokens)
        {
            AssertStateIsNotNull();

            tokens.Add(TokenForStartState(_builder.ToString()));

            _state = State.PendingOperator;
        }

        private void AddCurrentStateAsCompletionToken(List<Token> tokens)
        {
            AssertStateIsNotNull();

            tokens.Add(TokenForCompletionState(_builder.ToString()));

            _state = State.PendingOperator;
        }

        private void AddCurrentStateAsArrayStateToTokens(List<Token> tokens)
        {
            AssertStateIsNotNull();

            tokens.Add(TokenForArrayState(_builder.ToString()));

            _state = State.PendingOperator;
        }        

        private void AssertStateIsNotNull()
        {
            if (_builder.Length == 0) Exception("Zero length state not allowed.");
        }

        private void Exception(string message)
        {
            if (!message.EndsWith(".")) message = "{0}.".F(message);

            throw new InvalidOperationException("{0} Line {1} : Column {2}. Current State {3}".F(message, _rowNum, _colNum, _state));
        }

        private void HandleStartOperatorState(List<Token> tokens, char val)
        {
            if (_state != State.BuildingState && _state != State.PendingOperator) Exception("Unexpected '{0}' located.".F(val));

            if (_state == State.BuildingState) AddCurrentStateToTokens(tokens);
        }



        private Token TokenFor(Token.TokenType tokenType)
        {
            return Token.For(tokenType, _colNum, _rowNum);
        }

        private Token TokenFor(Token.TokenType tokenType, string data)
        {
            return Token.For(tokenType, data, _colNum, _rowNum);
        }

        private Token TokenForStartState(string data)
        {
            return Token.ForStartState(data, _colNum, _rowNum);
        }

        private Token TokenForCompletionState(string data)
        {
            return Token.ForCompletionState(data, _colNum, _rowNum);
        }

        private Token TokenForArrayState(string data)
        {
            return Token.ForArrayState(data, _colNum, _rowNum);
        }
    }
}
